/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.cloud.oracle.adm;

import com.oracle.bmc.adm.ApplicationDependencyManagementClient;
import com.oracle.bmc.adm.model.ApplicationDependency;
import com.oracle.bmc.adm.model.ApplicationDependencyVulnerabilitySummary;
import com.oracle.bmc.adm.model.CreateVulnerabilityAuditDetails;
import com.oracle.bmc.adm.model.SortOrder;
import com.oracle.bmc.adm.model.Vulnerability;
import com.oracle.bmc.adm.model.VulnerabilityAudit;
import com.oracle.bmc.adm.model.VulnerabilityAudit.LifecycleState;
import com.oracle.bmc.adm.model.VulnerabilityAuditConfiguration;
import com.oracle.bmc.adm.model.VulnerabilityAuditSummary;
import com.oracle.bmc.adm.requests.CreateVulnerabilityAuditRequest;
import com.oracle.bmc.adm.requests.ListApplicationDependencyVulnerabilitiesRequest;
import com.oracle.bmc.adm.requests.ListVulnerabilityAuditsRequest;
import com.oracle.bmc.adm.responses.CreateVulnerabilityAuditResponse;
import com.oracle.bmc.adm.responses.ListApplicationDependencyVulnerabilitiesResponse;
import com.oracle.bmc.adm.responses.ListVulnerabilityAuditsResponse;
import com.oracle.bmc.model.BmcException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.lsp.Diagnostic;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.cloud.oracle.OCIManager;
import org.netbeans.modules.project.dependency.ArtifactSpec;
import org.netbeans.modules.project.dependency.Dependency;
import org.netbeans.modules.project.dependency.DependencyResult;
import org.netbeans.modules.project.dependency.ProjectDependencies;
import org.netbeans.modules.project.dependency.Scopes;
import org.netbeans.modules.project.dependency.SourceLocation;
import org.netbeans.spi.lsp.ErrorProvider;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;

/**
 *
 * @author Petr Pisl
 */
@NbBundle.Messages({
    "# {0} - project name",
    "MSG_Audit_Pass=Vulnerability audit for project {0} is done.\nNo vulnerability was found.",
    "# {0} - project name",
    "MSG_Audit_Failed_One=Vulnerability audit for project {0} is done.\nOne vulnerability was found.\nThe vulnerability is listed in Problems window.",
    "# {0} - project name",
    "# {1} - number of vulnerabilities",
    "MSG_Audit_Failed_More=Vulnerability audit for project {0} is done.\n{1} vulnerabilities were found.\nThe vulnerabilities are listed in Problems window.",
    "# {0} - project name",
    "MSG_ListingAuditFailed=Obtaining newly created vulnerablity audit for project {0} failed.",
    "MSG_ListingVulnerabilitiesFailed=Obtaining vulnerabilities for newly created audit failed.",
    "# {0} - project name",
    "MSG_AuditIsRunning=Checking for audit of project {0}",
    "MSG_NotAvailable=Not available",
    "# {0} - Cvss V2 score",
    "# {1} - Cvss V3 score",
    "# {2} - Vulnerable dependency GAV",
    "MSG_Diagnostic=Vulnerability\n"
            + "  Cvss V2 Score: {0}\n"
            + "  Cvss V3 Score: {1}\n"
            + "  Caused by dependence: {2}",
    "# {0} - Cvss V2 score",
    "# {1} - Cvss V3 score",
    "# {2} - Vulnerable dependency GAV",
    "# {3} - Including dependency GAV",
    "MSG_Diagnostic_Included=Vulnerability\n"
            + "  Cvss V2 Score: {0}\n"
            + "  Cvss V3 Score: {1}\n"
            + "  Caused by dependence: {2}, included by {3}",
    "MSG_SearchingAuditReport=Searching for audit reports...",
    "MSG_AuditCollectDependencies=Collecting project dependencies...",
    "MSG_ExecuteAudit=Executing audit..."
})
 
@MimeRegistrations({
    @MimeRegistration(mimeType = "text/x-maven-pom+xml", service = ErrorProvider.class),
    @MimeRegistration(mimeType = "text/x-gradle+x-groovy", service = ErrorProvider.class)  
})

public class VulnerabilityWorker implements ErrorProvider{
    private static final RequestProcessor SOURCE_REFRESH_PROCESSOR = new RequestProcessor(VulnerabilityWorker.class.getName());
    
    private static final Logger LOG = Logger.getLogger(VulnerabilityWorker.class.getName());
    
    // PENDING: should be customizable from project configuration somehow.
    private static final String GOV_DETAIL_URL = "https://nvd.nist.gov/vuln/detail/";
    
    private static final String TAG_SUBPROJECT_PATH = "netbeans_cloud_oracle_SubProjectPath"; // NOI18N
    
    // @GuardedBy(self)
    private static final HashMap<Project, CacheItem> cache = new HashMap<>();
    
    // @GuardedBy(class)
    private static VulnerabilityWorker instance;

    private static String nonNull(String s) {
        return s == null ? "" : s;
    }
    
    /**
     * Holds mapping from vulnerable dependencies to the source. This is invalidated and
     * recomputed after each source change from the same dependency result.
     * This is computed for dependencies found in vulnerability items.
     */
    final static class SourceMapping {
        /**
         * For each reported Depdendency, its closest parent (including self) with source
         * information.
         */
        final Map<Dependency, Dependency> nodesWithSource = new HashMap<>();

        /**
         * Source locations for the dependencies. Note the SourceLocation may be implied,
         * so that it points to a different Dependnecy.
         */
        final Map<Dependency, SourceLocation> locations = new HashMap<>();
        
        /**
         * Set of files with reported locations. Usually just one.
         */
        final Set<FileObject> files = new HashSet<>();
    }


    /**
     * Records all vulnerabilities for a certain group-artifact-version. Collects all
     * locations (paths) in the dependency tree where the GAV appears.
     */
    final static class VulnerabilityItem {
        /**
         * GAV coordinates of the reported artifact
         */
        final String gav;
        /**
         * List of vulnerabilities found for this artifact GAV
         */
        final List<Vulnerability> reports;
        
        /**
         * All paths to the artifact through the dependency graph. Each List is a path
         * from the project root to the identified dependency.
         */
        final Set<List<Dependency>>  paths = new LinkedHashSet<>();
        
        public VulnerabilityItem(String gav, List<Vulnerability> reports) {
            this.gav = gav;
            this.reports = reports;
        }
    }
    
    private static String getDependencyId(Dependency d) {
        if (d.getProject() != null) {
            return d.getProject().getProjectId();
        } else if (d.getArtifact() != null) {
            return createGAV(d.getArtifact());
        } else {
            return d.toString();
        }
    }
    
    static Pair<Dependency, SourceLocation> findSource(DependencyResult dependencyResult, Dependency dependency) {
        SourceLocation sourcePath = null;
        Dependency sd = dependency;
        for (; sd != null; sd = sd.getParent()) {
            try {
                sourcePath = dependencyResult.getDeclarationRange(dependency, null);
                if (sourcePath != null) {
                    break;
                }
            } catch (IOException ex) {
                LOG.log(Level.WARNING, "Could not load dependency source", ex);
            }
        }
        return Pair.of(sd, sourcePath);
    }
    
    static class SourceMappingBuilder {
        private final DependencyResult dependencyResult;
        private final Map<String, VulnerabilityItem> itemIndex;
        private SourceMapping result = new SourceMapping();

        public SourceMappingBuilder(DependencyResult dependencyResult, Map<String, VulnerabilityItem> itemIndex) {
            this.dependencyResult = dependencyResult;
            this.itemIndex = itemIndex;
        }
        
        public SourceMapping build() {
            for (VulnerabilityItem item : itemIndex.values()) {
                for (List<Dependency> path : item.paths) {
                    Dependency node = path.get(path.size() - 1);
                    if (result.nodesWithSource.containsKey(node)) {
                        continue;
                    }
                    Pair<Dependency, SourceLocation> s = findSource(dependencyResult, node);
                    result.nodesWithSource.put(node, s.first());
                    if (s.first() == null || s.second() == null) {
                        continue;
                    }
                    SourceLocation l = s.second();
                    result.nodesWithSource.put(node, s.first());
                    if (l != null) {
                        result.locations.putIfAbsent(s.first(), l);
                        result.files.add(l.getFile());
                    }
                }
            }
            return result;
        }
    }

    
    /**
     * Builds the vulnerability indices from the vulnerability report and project dependencies. The outcome is "itemIdex" and
     * "sourceMapping" which are then copied to {@link CacheItem}.
     */
    static class CacheDataBuilder {
        private final DependencyResult dependencyResult;
        private final VulnerabilityReport report;
        
        private Map<String, VulnerabilityItem> itemIndex = new HashMap<>();
        private SourceMapping sourceMapping;
        private Set<String> uniqueDeps = new HashSet<>();

        public CacheDataBuilder(DependencyResult dependencyResult, VulnerabilityReport report) {
            this.dependencyResult = dependencyResult;
            this.report = report;
        }

        private void initVulnerabilityIndex() {
            this.itemIndex = new HashMap<>();
            for (ApplicationDependencyVulnerabilitySummary s: report.items) {
                VulnerabilityItem item = new VulnerabilityItem(s.getGav(), s.getVulnerabilities());
                itemIndex.put(s.getGav(), item);
            }
        }
        
        private void buildDependencyMap(Dependency dependency, List<Dependency> path, Set<String> pathToRoot) {
            String gav = getDependencyId(dependency);
            if (gav == null || !pathToRoot.add(gav)) {
                return;
            }
            uniqueDeps.add(gav);
            
            pathToRoot.add(gav);
            try {
                VulnerabilityItem item = itemIndex.get(gav);
                if (item != null) {
                    List<Dependency> p = new ArrayList<>(path);
                    p.add(dependency);
                    item.paths.add(p);
                }
                
                if (dependency != dependencyResult.getRoot()) {
                    path.add(dependency);
                }
                dependency.getChildren().forEach((childDependency) -> {
                    buildDependencyMap(childDependency, path, pathToRoot);
                });
                if (!path.isEmpty()) {
                    Dependency x = path.remove(path.size() - 1);
                    assert x == dependency;
                }
            } finally {
                pathToRoot.remove(gav);
            }
        }
        
        Map<String, VulnerabilityItem> build() {
            initVulnerabilityIndex();
            SourceLocation rootLocation = null;
            try {
                rootLocation = dependencyResult.getDeclarationRange(dependencyResult.getRoot(), null);
            } catch (IOException ex) {
                LOG.log(Level.WARNING, "Could not load dependency source", ex);
            }
            buildDependencyMap(this.dependencyResult.getRoot(), new ArrayList<>(), new HashSet<>());
            sourceMapping = new SourceMappingBuilder(dependencyResult, itemIndex).build();
            return itemIndex;
        }
    }
    
    /**
     * Cached information + watcher over the project file data. Will watch for dependency change event,
     * that is fired e.g. after project reload, and will REPLACE ITSELF in the cache + fire
     * event that causes LSP to re-evaluate errors for the affected project file(s).
     */
    static class CacheItem {
        private final Project project;
        private final DependencyResult dependencyResult;
        private final VulnerabilityReport report;

        // @GuardedBy(this) -- initialization only
        private Map<String, VulnerabilityItem> itemIndex;
        
        // @GuardedBy(this) -- initialization only
        private SourceMapping sourceMapping;
        /**
         * Number of dependencies
         */
        // @GuardedBy(this) -- initialization only
        private int uniqueDependencies;

        // @GuardedBy(this)
        private ChangeListener depChange;
        private ChangeListener sourceChange;
        
        // @GuardedBy(this)
        private RequestProcessor.Task pendingRefresh;
        
        public CacheItem(Project project, DependencyResult dependency, VulnerabilityReport report) {
            this.project = project;
            this.dependencyResult = dependency;
            this.report = report;
        }

        public DependencyResult getDependency() {
            return dependencyResult;
        }

        public VulnerabilityAuditSummary getAudit() {
            return report.summary;
        }

        public List<ApplicationDependencyVulnerabilitySummary> getVulnerabilities() {
            return report.items;
        }
        
        private synchronized void initialize() {
            if (itemIndex != null) {
                return;
            }
            startListening();
            CacheDataBuilder b = new CacheDataBuilder(dependencyResult, report);
            Map<String, VulnerabilityItem> items = b.build();
            this.itemIndex = b.itemIndex;
            this.sourceMapping = b.sourceMapping;
            this.uniqueDependencies =  b.uniqueDeps.size();
        }
        
        public Set<FileObject> getProblematicFiles() {
            if (getAudit().getIsSuccess()) {
                return Collections.EMPTY_SET;
            }
            initialize();
            return sourceMapping.files;
        }
        
        VulnerabilityItem findVulnerability(String gav) {
            initialize();
            return itemIndex.get(gav);
        }
        
        void refreshSourceMapping(RequestProcessor.Task t) {
            SourceMapping old;
            SourceMapping m = new SourceMappingBuilder(dependencyResult, itemIndex).build();
            Set<FileObject> files = new HashSet<>();
            synchronized (this) {
                // should block on this until t[0] is assigned
                if (pendingRefresh != t) {
                    return;
                }
                old = this.sourceMapping;
                sourceMapping = m;
            }
            if (old != null) {
                files.addAll(old.files);
            }
            Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
            files.addAll(m.files);
            reporter.diagnosticChanged(files, null);
        }
        
        void refreshDependencies(RequestProcessor.Task t) {
            boolean model;
            synchronized (cache) {
                CacheItem registered = cache.get(project);
                if (registered != this) {
                    return;
                }
            }
            synchronized (this) {
                // should block on this until t[0] is assigned
                if (pendingRefresh != t) {
                    return;
                }
                model = modelChanged || itemIndex == null;
                modelChanged = false;
            }
            if (model) {
                refreshProjectDependencies(t);
            } else {
                refreshSourceMapping(t);
            }
        }
        
        void refreshProjectDependencies(RequestProcessor.Task t) {
            DependencyResult dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
            LOG.log(Level.FINER, "{0} - dependencies refreshed", this);
            synchronized (this) {
                // should block on this until t[0] is assigned
                if (pendingRefresh != t) {
                    return;
                }
                modelChanged = false;
            }
            CacheItem novy = new CacheItem( project, dr, report);
            if (LOG.isLoggable(Level.FINER)) {
                LOG.log(Level.FINER, "{0} - trying to replace for {1}", new Object[] { this, novy });
            }
            if (replaceCacheItem(project, this, novy)) {
                novy.startListening();
                Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
                Set<FileObject> allFiles = new HashSet<>();
                allFiles.addAll(getProblematicFiles());
                allFiles.addAll(novy.getProblematicFiles());
                if (LOG.isLoggable(Level.FINER)) {
                    LOG.log(Level.FINER, "{0} - refreshing files: {1}", new Object[] { this, allFiles });
                }
                reporter.diagnosticChanged(allFiles, null);
            }
        }
        
        /**
         * True, if the project model changed; false, if only source has changed.
         */
        private boolean modelChanged;
        
        void scheduleSourceRefresh(ChangeEvent e) {
            // PENDING: enable when Maven and gradle dependency implementation stabilizaes on reading the 
            // actual source.
            // scheduleRefresh(false);
        }
        
        void scheduleDependencyRefresh(ChangeEvent e) {
            scheduleRefresh(true);
        }
        
        private void scheduleRefresh(boolean modelChanged) {
            synchronized (this) {
                if (pendingRefresh != null) {
                    pendingRefresh.cancel();
                }
                modelChanged |= modelChanged;
                RequestProcessor.Task[] task = new RequestProcessor.Task[1];
                if (LOG.isLoggable(Level.FINER)) {
                    LOG.log(Level.FINER, "{0} - scheduling refresh for {1}", new Object[] { this, project });
                }
                pendingRefresh = task[0] = SOURCE_REFRESH_PROCESSOR.create(() -> {
                    RequestProcessor.Task t;
                    synchronized (this) {
                        t = task[0];
                    }
                    refreshDependencies(t);
                });
                task[0].schedule(0);
            }
        }
        
        void startListening() {
            synchronized (this) {
                if (depChange == null) {
                    dependencyResult.addChangeListener(depChange = this::scheduleDependencyRefresh);
                    LOG.log(Level.FINER, "{0} - start listen for dependencies", this);
                    dependencyResult.addSourceChangeListener(sourceChange = this::scheduleSourceRefresh);
                }
            }
        }
        
        void stopListening() {
            synchronized (this) {
                if (depChange != null) {
                    dependencyResult.removeChangeListener(depChange);
                    // intentionally does not clean depChange, to make a subsequent startListening no-op.
                    LOG.log(Level.FINER, "{0} - stop listen for dependencies", this);
                    dependencyResult.removeSourceChangeListener(sourceChange);
                }
            }
        }
        
        Diagnostic createDiagnostic(Dependency owner, Dependency dependency, List<Dependency> path, Vulnerability vulnerability, SourceLocation declarationRange) {
            String ownerGav = owner == null ? null : getDependencyId(owner);
            String message = 
                ownerGav == null ? 
                    Bundle.MSG_Diagnostic(
                        formatCvssScore(vulnerability.getCvssV2Score()), 
                        formatCvssScore(vulnerability.getCvssV3Score()), 
                        getDependencyId(dependency)) :
                    Bundle.MSG_Diagnostic_Included(
                        formatCvssScore(vulnerability.getCvssV2Score()), 
                        formatCvssScore(vulnerability.getCvssV3Score()), 
                        getDependencyId(dependency),
                        ownerGav);
            SourceLocation fDeclarationRange = declarationRange;
            Diagnostic.Builder builder = Diagnostic.Builder.create(() -> fDeclarationRange.getStartOffset(), () -> fDeclarationRange.getEndOffset(), message);
            builder.setSeverity(Diagnostic.Severity.Warning);
            try {
                builder.setCodeDescription(new URL(GOV_DETAIL_URL + vulnerability.getId()));
            } catch (MalformedURLException ex) {
                // perhaps should not happen at all
                LOG.log(Level.INFO, "Could not link to vulnerability: {0}", vulnerability.getId());
            }
            
            // TODO: when Diagnostic supports (some) ID, change to report the unique id separately, now it is embedded into the diagnostic code.
            String pathString = path.stream().map(d -> getDependencyId(d)).collect(Collectors.joining("/"));
            builder.setCode(vulnerability.getId() + "~~" + pathString);
            return builder.build();
        }
        
        public List<Diagnostic> getDiagnosticsForFile(FileObject file) {
            if (LOG.isLoggable(Level.FINER)) {
                LOG.log(Level.FINER, "{0} getDiagnostics called for {1}", new Object[] { this, file });
            }
            if (getVulnerabilities() == null || getVulnerabilities().isEmpty()) {
                return null;
            }
            
            initialize();
            
            List<Diagnostic> result = new ArrayList<>();
            SourceLocation containerLocation = null;
            if (!sourceMapping.files.contains(file)) {
                return null;
            }
            if (dependencyResult.getProject().getProjectDirectory() != file.getParent()) {
                return null;
            }
            
            for (VulnerabilityItem item : this.itemIndex.values()) {
                boolean unreported = true;
                Set<Dependency> anchors = new HashSet<>();
                for (List<Dependency> path : item.paths) {
                    Dependency dependency = path.get(path.size() - 1);
                    
                    SourceLocation declarationRange = null;

                    String ownerGav = null;                    
                    Dependency withSource = sourceMapping.nodesWithSource.get(dependency);
                    if (withSource != null) {
                        declarationRange = sourceMapping.locations.get(withSource); // XXX, neprepisovat nullem
                    }
                    if (declarationRange == null) {
                        // will deal with unampped vulnerabilities later.
                        continue;
                    }
                    Dependency owner = withSource;
                    if (withSource != dependency) {
                        // the dependency result may not report implied sources, so fallback
                        // to the closest parent artifact
                        ownerGav = getDependencyId(withSource);
                    }

                    if (declarationRange.getImpliedBy() instanceof Dependency) {
                        owner = (Dependency)declarationRange.getImpliedBy();
                        ownerGav = getDependencyId(owner);
                    }
                    if (!anchors.add(owner)) {
                        // do not report additional "foo" included from "bar", if already reported.
                        continue;
                    }
                    for (Vulnerability vulnerability : item.reports) {
                        unreported = false;
                        result.add(createDiagnostic(owner, dependency, path, vulnerability, declarationRange));
                    }
                }
                if (unreported && !item.paths.isEmpty()) {
                    // if the vulnerability item was matched initially and now it is not found, or mapped to a source,
                    // the user may have removed it from the project without running the analysis again - it should not be reported, it will eventually reappear
                    // after next analysis.
                    // But if it is not in the original map for some reason, report on the container location
                    if (!report.getMappedVulnerabilities().contains(item.gav)) {
                        if (containerLocation == null) {
                            try {
                                containerLocation = dependencyResult.getDeclarationRange(dependencyResult.getRoot(), DependencyResult.PART_CONTAINER);
                            } catch (IOException ex) {
                                LOG.log(Level.WARNING, "Could not load container location", ex);
                            }
                            if (containerLocation == null) {
                                containerLocation = new SourceLocation(project.getProjectDirectory(), 0, 0, null);
                            }
                        }
                        for (Vulnerability vulnerability : item.reports) {
                            List<Dependency> path = item.paths.iterator().next();
                            Dependency dependency = path.get(path.size() - 1);
                            result.add(createDiagnostic(null, dependency, path, vulnerability, containerLocation));
                        }
                    }
                }
            }
            return result;
        }
        
        private String formatCvssScore(Float value) {
            if (value != null) {
                return String.format("%.2f", value);
            }
            return Bundle.MSG_NotAvailable();
        }
    }
    
    private static boolean replaceCacheItem(Project p, CacheItem novy) {
        return replaceCacheItem(p, null, novy);
    }
    
    private static boolean replaceCacheItem(Project p, CacheItem old, CacheItem novy) {
        CacheItem registered;
        synchronized (cache) {
            registered = cache.get(p);
            if (old != null) {
                if (old != registered) {
                    old.stopListening();
                    return false;
                }
            } 
            if (registered != null) {
                registered.stopListening();
            }
            cache.put(novy.project, novy);
        }
        return true;
    }
    
    private VulnerabilityWorker() {
    }

    public static VulnerabilityWorker getInstance() {
        synchronized (VulnerabilityWorker.class) {
            if (instance == null) {
                instance = new VulnerabilityWorker();

            }
        }
        return instance;
    }

    /**
     * @param project
     * @param forceAudit
     * @return Returns the audit ID, or {@code null} if no audit is present.
     */
    public String findVulnerability(Project project, AuditOptions auditOptions) throws AuditException {
        AuditResult r = vulnerabilityAudit(project, auditOptions);
        return r.getAuditId();
    }
    
    public AuditResult vulnerabilityAudit(Project project, AuditOptions auditOptions) throws AuditException {
        if (auditOptions == null) {
            auditOptions = new AuditOptions();
        }
        LOG.log(Level.FINER, "Trying to obtain audit for project {0}, force:{1}", new Object[] { project, auditOptions.isForceAuditExecution() });
        final String projectDisplayName = ProjectUtils.getInformation(project).getDisplayName();
        KnowledgeBaseItem kbItem = getKnowledgeBaseForProject(project);
        if (kbItem == null) {
            return null;
        }
        
        // remove from the cache old values
        ProgressHandle progressHandle = ProgressHandle.createHandle(Bundle.MSG_AuditIsRunning(projectDisplayName));
        AtomicBoolean remoteCall = new AtomicBoolean(false);
        if (auditOptions.getAuditName() == null) {
            auditOptions.setAuditName(projectDisplayName);
        }
        try {
            return doFindVulnerability(project, kbItem, 
                    projectDisplayName, auditOptions, progressHandle, remoteCall);
        } finally {
            if (remoteCall.get()) {
                progressHandle.close();
            }
            kbItem.refresh();
        }
    }
    
    private static String findSubprojectPath(Project project) {
        Project p2 = ProjectUtils.rootOf(project);
        if (p2 == project) {
            return "";
        } else {
            return FileUtil.getRelativePath(p2.getProjectDirectory(), project.getProjectDirectory()).replace('/', '.');
        }
    }

    private AuditResult doFindVulnerability(Project project, KnowledgeBaseItem knowledgeBase, String projectDisplayName, AuditOptions auditOptions,
            ProgressHandle progressHandle, AtomicBoolean remoteCall) throws AuditException {
        List<ApplicationDependency> result = new ArrayList<>();
        
        CacheItem cacheItem = null;
        VulnerabilityReport savedAudit = null;
        
        boolean auditDone = false;

        DependencyResult dr = null;
        String subprojectPath = findSubprojectPath(project);        
        if (!auditOptions.isForceAuditExecution()) {
            if (!auditOptions.isDisableCache()) {
                try {
                    savedAudit = AuditCache.getInstance().loadAudit(project, knowledgeBase.getKey().getValue());
                } catch (IOException ex) {
                    LOG.log(Level.WARNING, "Could not load cached audit data", ex); 
                }
            }
            if (savedAudit == null) {
                // attempt to find an active most recent audit:
                remoteCall.set(true);
                progressHandle.start();
                progressHandle.progress(Bundle.MSG_SearchingAuditReport());
                try (ApplicationDependencyManagementClient admClient
                        = new ApplicationDependencyManagementClient(OCIManager.getDefault().getActiveProfile(knowledgeBase).getConfigProvider())) {
                    ListVulnerabilityAuditsRequest request = ListVulnerabilityAuditsRequest
                            .builder()
                            .compartmentId(knowledgeBase.getCompartmentId())
                            .knowledgeBaseId(knowledgeBase.getKey().getValue())
                            .lifecycleState(LifecycleState.Active)
                            .sortBy(ListVulnerabilityAuditsRequest.SortBy.TimeCreated)
                            .limit(200)
                            .sortOrder(SortOrder.Desc).build();
                    ListVulnerabilityAuditsResponse response = admClient.listVulnerabilityAudits(request);
                    if (!response.getVulnerabilityAuditCollection().getItems().isEmpty()) {
                        VulnerabilityAuditSummary found = null;
                        D: while (true) {
                            for (VulnerabilityAuditSummary summary : response.getVulnerabilityAuditCollection().getItems()) {
                                String p = summary.getFreeformTags().get(TAG_SUBPROJECT_PATH);
                                if (subprojectPath.equals(p)) {
                                    found = summary;
                                    break D;
                                }
                            }
                            if (response.getOpcNextPage() == null) {
                                break;
                            }
                            request = ListVulnerabilityAuditsRequest
                                .builder().copy(request).page(response.getOpcNextPage()).build();
                            response = admClient.listVulnerabilityAudits(request);
                        }
                        progressHandle.progress(Bundle.MSG_AuditCollectDependencies());
                        dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
                        convert(dr.getRoot(), new HashMap<>(), result);
                        cacheItem = fetchVulnerabilityItems(project, admClient, dr, found, projectDisplayName);
                        savedAudit = new VulnerabilityReport(cacheItem.getAudit(), cacheItem.getVulnerabilities());
                    }
                } catch (BmcException ex) {
                    LOG.log(Level.FINE, "Unable to list newest audit for knowledgebase {0}, compartment {1}", new Object[] {
                        knowledgeBase.getKey().getValue(), knowledgeBase.getCompartmentId()
                    });
                }
            }
        }
        
        if (savedAudit == null && (auditOptions.isRunIfNotExists() || auditOptions.isForceAuditExecution())) {
            if (remoteCall.compareAndSet(false, true)) {
                progressHandle.start();
            }
            if (dr == null) {
                progressHandle.progress(Bundle.MSG_AuditCollectDependencies());
                dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
                convert(dr.getRoot(), new HashMap<>(), result);
            }
            try (ApplicationDependencyManagementClient admClient
                    = new ApplicationDependencyManagementClient(OCIManager.getDefault().getActiveProfile(knowledgeBase).getConfigProvider())) {
                final VulnerabilityAuditConfiguration auditConfiguration = VulnerabilityAuditConfiguration
                        .builder()
                        .maxPermissibleCvssV2Score(1f)
                        .maxPermissibleCvssV3Score(1f)
                        .exclusions(Collections.unmodifiableList(Collections.EMPTY_LIST))
                        .build();
                final CreateVulnerabilityAuditDetails auditDetails = CreateVulnerabilityAuditDetails
                        .builder()
                        .compartmentId(knowledgeBase.getCompartmentId())
                        .knowledgeBaseId(knowledgeBase.getKey().getValue())
                        .displayName(auditOptions.getAuditName().replaceAll("[^A-Za-z0-9-_]", "_")) // remove offending characters
                        .buildType(VulnerabilityAudit.BuildType.Maven)
                        .configuration(auditConfiguration)
                        .applicationDependencies(result)
                        .freeformTags(Map.of(TAG_SUBPROJECT_PATH, subprojectPath))
                        .build();
                progressHandle.progress(Bundle.MSG_ExecuteAudit());
                CreateVulnerabilityAuditResponse response = admClient.createVulnerabilityAudit(CreateVulnerabilityAuditRequest
                        .builder()
                        .createVulnerabilityAuditDetails(auditDetails)
                        .build());
                if (response.get__httpStatusCode__() != 201 && response.get__httpStatusCode__() != 200) {
                    throw new BmcException(response.get__httpStatusCode__(), null, null, response.getOpcRequestId());
                }
                // audit is ok
                cacheItem = waitToAuditFinish(project, admClient, dr, response.getVulnerabilityAudit(), projectDisplayName);
                auditDone = true;
            } catch (BmcException exc) {
                throw new AuditException(exc.getStatusCode(), exc.getOpcRequestId(), exc.getMessage(), exc);
            } finally {
                progressHandle.finish();
            }
        } else if (savedAudit != null && cacheItem == null) {
            if (dr == null) {
                // not really a remote call, but lengthy anyway...
                if (remoteCall.compareAndSet(false, true)) {
                    progressHandle.start();
                }
                progressHandle.progress(Bundle.MSG_AuditCollectDependencies());
                dr = ProjectDependencies.findDependencies(project, ProjectDependencies.newQuery(Scopes.RUNTIME));
                convert(dr.getRoot(), new HashMap<>(), result);
            }
            cacheItem = new CacheItem(project, dr, savedAudit);
        }

        if (cacheItem != null) {
            synchronized (cache) {
                replaceCacheItem(project, cacheItem);
            }

            Set<FileObject> problematicFiles = new HashSet<>();
            problematicFiles.addAll(cacheItem.getProblematicFiles());

            String message;
            if (cacheItem.getAudit().getIsSuccess()) {
                message = Bundle.MSG_Audit_Pass(projectDisplayName);
                problematicFiles.addAll(dr.getDependencyFiles());
            } else if(cacheItem.getAudit().getVulnerableArtifactsCount() == 1) {
                message = Bundle.MSG_Audit_Failed_One(projectDisplayName);
            } else {
                message = Bundle.MSG_Audit_Failed_More(projectDisplayName, cacheItem.getAudit().getVulnerableArtifactsCount());
            }
            if (auditDone && auditOptions.isDisplaySummary()) {
                DialogDisplayer.getDefault().notifyLater(
                                new NotifyDescriptor.Message(message, cacheItem.getAudit().getIsSuccess() ? NotifyDescriptor.INFORMATION_MESSAGE : NotifyDescriptor.WARNING_MESSAGE));
            }
            Diagnostic.ReporterControl reporter = Diagnostic.findReporterControl(Lookup.getDefault(), project.getProjectDirectory());
            reporter.diagnosticChanged(problematicFiles, null);
            
            List<ArtifactSpec> arts = new ArrayList<>();
            Set<String> gavs = new HashSet<>();
            for (ApplicationDependencyVulnerabilitySummary s : cacheItem.getVulnerabilities()) {
                if (!gavs.add(s.getGav())) {
                    continue;
                }
                VulnerabilityItem i = cacheItem.findVulnerability(s.getGav());
                if (i == null || i.paths.isEmpty()) {
                    continue;
                }
                List<Dependency> p = i.paths.iterator().next();
                Dependency d = p.get(p.size() - 1);
                if (d == null) {
                    continue;
                }
                ArtifactSpec spec = d.getArtifact();
                if (spec != null) {
                    arts.add(spec);
                }
            }
            AuditResult res = new AuditResult(project, projectDisplayName, cacheItem.report.summary.getId(), 
                    cacheItem.uniqueDependencies, cacheItem.getAudit().getVulnerableArtifactsCount(), 
                    arts);
            return res;
        } else {
            // indicate somehow the KB exists, but empty
            return new AuditResult(project, projectDisplayName, "",  // NOI18N
                    0, 0, Collections.emptyList());
        }
    }
    
    public static KnowledgeBaseItem getKnowledgeBaseForProject(Project project) {
        ProjectVulnerability vs = project.getLookup().lookup(ProjectVulnerability.class);
        return vs != null ? vs.getProjectKnowledgeBase() : null;
    }
    
    private int convert(Dependency dependency, Map<String, Integer> gavIndex, List<ApplicationDependency> result) {
        String gav = createGAV(dependency.getArtifact());
        if (gav == null) {
            return -1;
        }
        Integer n = gavIndex.get(gav);
        if (n != null) {
            return n;
        }
        gavIndex.put(gav, n = gavIndex.size() + 1);
        ApplicationDependency.Builder builder = ApplicationDependency.builder();
        builder.gav(gav);
        builder.nodeId(Integer.toString(n));
        
        List<String> childrenNodeIds = new ArrayList<>(dependency.getChildren().size());
        for (Dependency childDependency : dependency.getChildren()) {
            int cid = convert(childDependency, gavIndex, result);
            if (cid != -1) {
                childrenNodeIds.add(Integer.toString(cid));
            }
        }
        builder.applicationDependencyNodeIds(childrenNodeIds);
        result.add(builder.build());
        return n;
    }

    private static String createGAV(ArtifactSpec artifact) {
        if (artifact == null) {
            return null;
        }
        // use a random constant that could be sufficient to hold gav text (micronaut-core has max 86)
        StringBuilder sb = new StringBuilder(120); 
        sb.append(String.format("%s:%s:%s", 
                nonNull(artifact.getGroupId()), 
                nonNull(artifact.getArtifactId()),
                nonNull(artifact.getVersionSpec()))
        );
        // OCI API cannot accept classifier

        return sb.toString();
    }

    private CacheItem waitToAuditFinish(Project project, ApplicationDependencyManagementClient client, 
            DependencyResult dr, VulnerabilityAudit audit, String projectName) throws AuditException {                
        ListVulnerabilityAuditsRequest request = ListVulnerabilityAuditsRequest.builder()
                .knowledgeBaseId(audit.getKnowledgeBaseId()).id(audit.getId()).build();
        VulnerabilityAuditSummary auditSummary;
        boolean first = true;
        do {
            if (first) {
                first = false;
            } else {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
            ListVulnerabilityAuditsResponse response = client.listVulnerabilityAudits(request);
            if (response.get__httpStatusCode__() != 200) {
                throw new AuditException(response.get__httpStatusCode__(), response.getOpcRequestId(), Bundle.MSG_ListingAuditFailed(projectName), null);
            }
            List<VulnerabilityAuditSummary> items = response.getVulnerabilityAuditCollection().getItems();
            auditSummary = items.get(0);
        } while (auditSummary.getLifecycleState() == VulnerabilityAudit.LifecycleState.Creating);
        return fetchVulnerabilityItems(project, client, dr, auditSummary, projectName);
    }
    
    private CacheItem fetchVulnerabilityItems(Project project, ApplicationDependencyManagementClient client, 
            DependencyResult dr, VulnerabilityAuditSummary auditSummary, String projectName) {
        List<ApplicationDependencyVulnerabilitySummary> items = new ArrayList<>();
        if (auditSummary.getVulnerableArtifactsCount() > 0) {
            String nextPage = null;
            do {
                ListApplicationDependencyVulnerabilitiesRequest.Builder b = ListApplicationDependencyVulnerabilitiesRequest.builder().vulnerabilityAuditId(auditSummary.getId());
                if (nextPage != null) {
                    b.page(nextPage);
                }
                ListApplicationDependencyVulnerabilitiesRequest advRequest = b.build();
                ListApplicationDependencyVulnerabilitiesResponse vulners = client.listApplicationDependencyVulnerabilities(advRequest);
                if (vulners.get__httpStatusCode__() == 200) {
                    vulners.getApplicationDependencyVulnerabilityCollection().getItems().stream().filter(v -> !v.getVulnerabilities().isEmpty())
                            .forEach(v -> items.add(v));
                } else {
                    ErrorUtils.processError(vulners, Bundle.MSG_ListingVulnerabilitiesFailed());
                    return null;
                }
                nextPage = vulners.getOpcNextPage();
            } while (nextPage != null);
        }
        // Make an initial scan for the dependency locations in the Dependency report. 
        Set<String> mapped = new HashSet<>();
        VulnerabilityReport report = new VulnerabilityReport(auditSummary, items);
        CacheItem cache = new CacheItem(project, dr, report);
        for (ApplicationDependencyVulnerabilitySummary v : items) {
            List<Vulnerability> vulnerabilities = v.getVulnerabilities();
            if (!vulnerabilities.isEmpty()) {
                VulnerabilityItem item = cache.findVulnerability(v.getGav());
                if (item != null && !item.paths.isEmpty()) {
                    mapped.add(v.getGav());
                }
            }
        }
        report.setMappedVulnerabilities(mapped);
        
        try {
            AuditCache.getInstance().cacheAuditResults(project, report);
        } catch (IOException ex) {
            LOG.log(Level.WARNING, "Could not cache audit results for knowledgebase {0}, compartment {1}, project {2}", new Object[] {
                auditSummary.getKnowledgeBaseId(), auditSummary.getCompartmentId(),
                projectName
            });
            LOG.log(Level.WARNING, "The exception was: ", ex);
        }
        return cache;
    }
    
    @Override
    public List<? extends Diagnostic> computeErrors(Context context) {
        if (context.errorKind() != ErrorProvider.Kind.ERRORS) {
            return Collections.emptyList();
        }
        List<Diagnostic> result = new ArrayList<>();
        Collection<CacheItem> items;
        
        synchronized (cache) {
            items = new ArrayList<>(cache.values());
        }
        for (CacheItem cacheItem : items) {
            List<Diagnostic> diagnostics = cacheItem.getDiagnosticsForFile(context.file());
            if (diagnostics != null) {
                result.addAll(cacheItem.getDiagnosticsForFile(context.file()));
            }
        }
        return result;
    }
}
